﻿using System;

namespace TLang.Parsers
{
    using Ast;
    using System.Collections.Generic;

    public class Lexer
    {

        private readonly String _file;
        private readonly String _text;

        // current offset indicators
        private int _offset;
        private int _line;
        private int _col;


        public Lexer(string text, String file)
        {
            if (string.IsNullOrEmpty(file))
            {
                this._file = String.Empty;
                this._text = text ?? String.Empty;
            }
            else if(string.IsNullOrEmpty(text))
            {
                this._file = file;
                this._text = Util.ReadFile(file);

                if (this._text == null)
                {
                    throw Util.GeneralError("failed to read file: " + file);
                }
            }
            this._offset = 0;
            this._line = 0;
            this._col = 0;
        }
        
        private void Forward()
        {
            if (_text[_offset] == '\n')
            {
                _line++;
                _col = 0;
                _offset++;
            }
            else
            {
                _col++;
                _offset++;
            }
        }


        private void Skip(int n)
        {
            for (int i = 0; i < n; i++)
            {
                Forward();
            }
        }


        private bool SkipSpaces()
        {
            bool found = false;

            while (_offset < _text.Length && _text[_offset].IsWhiteSpace())
            {
                found = true;
                Forward();
            }
            return found;
        }


        private bool SkipComments()
        {
            bool found = false;

            if (_text.StartsWith(Constants.LineComment, _offset))
            {
                found = true;

                // skip to line end
                while (_offset < _text.Length && _text[_offset] != '\n')
                {
                    Forward();
                }
                if (_offset < _text.Length)
                {
                    Forward();
                }
            }
            return found;
        }


        private void SkipSpacesAndComments()
        {
            while (SkipSpaces() || SkipComments())
            {
                // actions are performed by skipSpaces() and skipComments()
            }
        }


        private Node ScanString()
        {
            int start = _offset;
            int startLine = _line;
            int startCol = _col;
            Skip(Constants.StringStart.Length);    // skip quote mark

            while (true)
            {
                // detect runaway strings at end of file or at newline
                if (_offset >= _text.Length || _text[_offset] == '\n')
                {
                    throw new ParserException("runaway string", startLine, startCol, _offset);
                }

                // end of string
                else if (_text.StartsWith(Constants.StringEnd, _offset))
                {
                    Skip(Constants.StringEnd.Length);    // skip quote mark
                    break;
                }

                // skip any char after STRING_ESCAPE
                else if (_text.StartsWith(Constants.StringEscape, _offset) && _offset + 1 < _text.Length)
                {
                    Skip(Constants.StringEscape.Length + 1);
                }

                // other characters (string content)
                else
                {
                    Forward();
                }
            }

            int end = _offset;
            String content = _text.Substring(
                    start + Constants.StringStart.Length,
                    end - Constants.StringEnd.Length - start - Constants.StringStart.Length);

            return new StringNode(content, _file, start, end, startLine, startCol);
        }

        private Node ScanChar()
        {
            int start = _offset;
            int startLine = _line;
            int startCol = _col;
            Skip(1);    // skip ' mark
            char c = _text[_offset];
            if (c == Constants.CharEscape)
            {
                Skip(1);
                c = _text[_offset];
            }
            if (_text[_offset + 1] != Constants.CharEnd)
            {
                throw new ParserException("runaway char", startLine, startCol, _offset);
            }
            Skip(2);    // skip ' mark
            return new CharNode(c, _file, start, _offset, startLine, startCol);
        }


        private Node ScanNumber()
        {
            int start = _offset;
            int startLine = _line;
            int startCol = _col;

            while (_offset < _text.Length && _text[_offset].IsNumberChar())
            {
                Forward();
            }

            String content = _text.Substring(start, _offset - start);

            IntNumNode intNum = IntNumNode.Parse(content, _file, start, _offset, startLine, startCol);
            if (intNum != null)
            {
                return intNum;
            }
            else
            {
                FloatNumNode floatNum = FloatNumNode.Parse(content, _file, start, _offset, startLine, startCol);
                if (floatNum != null)
                {
                    return floatNum;
                }
                else
                {
                    throw new ParserException("incorrect number format: " + content, startLine, startCol, start);
                }
            }
        }

        private Node ScanIntNumber()
        {
            int start = _offset;
            int startLine = _line;
            int startCol = _col;

            while (_offset < _text.Length && Char.IsDigit(_text[_offset]))
            {
                Forward();
            }

            String content = _text.Substring(start, _offset - start);

            IntNumNode intNum = IntNumNode.Parse(content, _file, start, _offset, startLine, startCol);
            if (intNum != null)
            {
                return intNum;
            }
            throw new ParserException("incorrect int number format: " + content, startLine, startCol, start);
        }

        private Node ScanNameOrKeyword()
        {
            int start = _offset;
            int startLine = _line;
            int startCol = _col;

            while (_offset < _text.Length && _text[_offset].IsIdentifierChar())
            {
                Forward();
            }

            char cur = _offset < _text.Length ? _text[_offset] : Char.MinValue;
            String content = _text.Substring(start, _offset - start);
            if (content.StartsWith(Constants.ColonChar))
            {
                if (cur == Constants.DotChar)
                {
                    throw new ParserException($"syntax error: keyword can't contains '{Constants.DotChar}'", startLine, startCol, start);
                }
                return new KeywordNode(content.Substring(1), _file, start, _offset, startLine, startCol);
            }
            else
            {
                string id = content;
                NameNode nameNode = new NameNode(id, _file, start, _offset, startLine, startCol);
                // Case DotNode
                if (cur == Constants.DotChar)
                {
                    List<Node> elements = new List<Node>
                    {
                        nameNode
                    };

                    for (Node dotPart = NextDotPart(); dotPart != null; dotPart = NextDotPart())
                    {
                        elements.AddRange(FlattenDotNode(dotPart));
                    }
                    return new DotNode(elements, _file, start, _offset, startLine, startCol);
                }
                return nameNode;
            }
        }

        private static List<Node> FlattenDotNode(Node node)
        {
            List<Node> list = new List<Node>();
            if (!(node is DotNode))
            {
                list.Add(node);
            }
            else
            {
                while (node is DotNode dotNode)
                {
                    foreach (var item in dotNode.Elements)
                    {
                        list.AddRange(FlattenDotNode(item));
                        node = item;
                    }
                }
            }
            return list;
        }

        /**
         * Helper for nextNode, which does the real work
         *
         * @return a Node or null if file ends
         */
        public Node NextNode(int depth, bool dontPaseFloatNumber = false)
        {
            Node first = NextToken(dontPaseFloatNumber);

            // end of file
            if (first == null)
            {
                return null;
            }

            if (first.IsOpen())
            {   // try to get matched (...)
                List<Node> elements = new List<Node>();
                Node next;
                for (next = NextNode(depth + 1);
                     !first.IsMatch(next);
                     next = NextNode(depth + 1))
                {
                    if (next == null)
                    {
                        throw new ParserException("unclosed delimeter till end of file: " + first.ToString(), first);
                    }
                    else if (next.IsClose())
                    {
                        throw new ParserException("unmatched closing delimeter: " +
                                next.ToString() + " does not close " + first.ToString(), next);
                    }
                    else
                    {
                        elements.Add(next);
                    }
                }
                return new TupleNode(elements, first, next, first.File, first.Start, next.End, first.Line, first.Col);
            }
            else if (depth == 0 && first.IsClose())
            {
                throw new ParserException("unmatched closing delimeter: " + first.ToString() +
                        " does not close any open delimeter", first);
            }
            else
            {
                return first;
            }
        }

        private Node NextDotPart()
        {
            if (_offset >= _text.Length)
            {
                return null;
            }
            char cur = _text[_offset];
            if (cur != Constants.DotChar)
            {
                return null;
            }
            while (true)
            {
                Forward();
                cur = _text[_offset];
                if (cur != Constants.DotChar)
                {
                    break;
                }
            }
            return NextNode(0, dontPaseFloatNumber: true);
        }

        /// <summary>
        /// next delimiter string number name or keyword
        /// <para>null when end of file</para>
        /// </summary>
        /// <returns>
        /// </returns>
        private Node NextToken(bool dontPaseFloatNumber)
        {

            SkipSpacesAndComments();

            // end of file
            if (_offset >= _text.Length)
            {
                return null;
            }

            // case 1. delimiters
            char cur = _text[_offset];
            if (cur.IsDelimiter())
            {
                Node ret = new DelimeterNode(cur.ToString(), _file, _offset, _offset + 1, _line, _col);
                Forward();
                return ret;
            }

            // case 2. string
            if (_text.StartsWith(Constants.StringStart, _offset))
            {
                return ScanString();
            }

            if (cur == Constants.CharStart)
            {
                return ScanChar();
            }

            if (dontPaseFloatNumber)
            {
                // case 3. number
                if (Char.IsDigit(_text[_offset]))
                {
                    return ScanIntNumber();
                }
            }
            else
            {
                // case 3. number
                if (Char.IsDigit(_text[_offset])
                    || ((_text[_offset] == Constants.PositiveChar || _text[_offset] == Constants.NegativeChar)
                         && _offset + 1 < _text.Length && Char.IsDigit(_text[_offset + 1])))
                {
                    return ScanNumber();
                }
            }

            // case 4. name or keyword
            if (_text[_offset].IsIdentifierChar())
            {
                return ScanNameOrKeyword();
            }

            // case 5. syntax error
            throw new ParserException("unrecognized syntax: " + _text.Substring(_offset, 1),
                    _line, _col, _offset);
        }
    }
}
